這篇研究是 MDSec 在 2021 年發表的文章,作者實作的 Dark LoadLibrary 可以說是 Reflective Loader 的進化版,在 DLL 載入時 Linking Internal Structures 的部分做的更加完整,但也還是有未完成的部分。我想透過分析程式碼,分享作者在設計 DLL Loading 的部分做了哪些改良 。
首先,作者給了一張圖比較了 Reflective Loader 和 Dark LoadLibrary 之間的差異。
(ref: https://www.mdsec.co.uk/2021/06/bypassing-image-load-kernel-callbacks/)
圖中可以發現,雖然 Reflective Loader 可以繞過 LoadImageNotifyRoutine
,但是在後續沒辦法對載入的 module 操作使用 GetProcAddress 和 GetModuleHandle,載入後的 DLL 也和一般 LoadLibrary 載入的 DLL 不同。
所以其實作者的目標是做出真正的 DLL Loader。
根據原文,DarkLoadLibrary 可以分解成幾個步驟:
一開始會使用這兩個 function 先取得必要的 APIs
DarkLoadLibrary 有兩種 flag 可以設定:
結果都是把整個 DLL 讀進 buffer 中存放,之後便會檢查:
PDARKMODULE DarkLoadLibrary(
DWORD dwFlags,
LPCWSTR lpwBuffer,
LPVOID lpFileBuffer,
DWORD dwLen,
LPCWSTR lpwName
)
{
HEAPALLOC pHeapAlloc = (HEAPALLOC)GetFunctionAddress(IsModulePresent(L"Kernel32.dll"), "HeapAlloc");
GETPROCESSHEAP pGetProcessHeap = (GETPROCESSHEAP)GetFunctionAddress(IsModulePresent(L"Kernel32.dll"), "GetProcessHeap");
/*
TODO:
I would really love to stop using error messages that need this.
All the other safe versions of wsprintfW are located in the CRT,
which is an issue if there is no CRT in the process.
For now let us hope nobody will pass a name larger than 500 bytes. :/
*/
WSPRINTFW pwsprintfW = (WSPRINTFW)GetFunctionAddress(IsModulePresent(L"User32.dll"), "wsprintfW");
PDARKMODULE dModule = (DARKMODULE*)pHeapAlloc(pGetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(DARKMODULE));
if (!dModule)
return NULL;
dModule->bSuccess = FALSE;
dModule->bLinkedToPeb = TRUE;
// get the DLL data into memory, whatever the format it's in
switch (LOWORD(dwFlags))
{
case LOAD_LOCAL_FILE:
if (!ParseFileName(dModule, lpwBuffer) || !ReadFileToBuffer(dModule))
{
goto Cleanup;
}
break;
case LOAD_MEMORY:
dModule->dwDllDataLen = dwLen;
dModule->pbDllData = lpFileBuffer;
/*
This is probably a hack for the greater scheme but lol
*/
dModule->CrackedDLLName = lpwName;
dModule->LocalDLLName = lpwName;
if (lpwName == NULL)
goto Cleanup;
break;
default:
break;
}
if (dwFlags & NO_LINK)
dModule->bLinkedToPeb = FALSE;
// is there a module with the same name already loaded
if (lpwName == NULL)
{
lpwName = dModule->CrackedDLLName;
}
HMODULE hModule = IsModulePresent(lpwName);
if (hModule != NULL)
{
dModule->ModuleBase = (ULONG_PTR)hModule;
dModule->bSuccess = TRUE;
goto Cleanup;
}
// make sure the PE we are about to load is valid
if (!IsValidPE(dModule->pbDllData))
{
dModule->ErrorMsg = (wchar_t*)pHeapAlloc(pGetProcessHeap(), HEAP_ZERO_MEMORY, 500);
if (!dModule->ErrorMsg)
goto Cleanup;
pwsprintfW(dModule->ErrorMsg, TEXT("Data is an invalid PE: %s"), lpwName);
goto Cleanup;
}
這邊的流程其實跟 Reflective Loader 差不多,分配 NtHeaders->OptionalHeader.SizeOfImage 大小的空間,再將 section 複製到對應的位置
// map the sections into memory
if (!MapSections(dModule))
{
dModule->ErrorMsg = (wchar_t*)pHeapAlloc(pGetProcessHeap(), HEAP_ZERO_MEMORY, 500);
if (!dModule->ErrorMsg)
goto Cleanup;
pwsprintfW(dModule->ErrorMsg, TEXT("Failed to map sections: %s"), lpwName);
goto Cleanup;
}
如果有 relocation 的話,也會和 Reflective Loader 一樣修正 relocation
這個步驟會解析一些該 DLL 的 Import table,並載入相依的 DLL 和其中的 imported functions。
// handle the import tables
if (!ResolveImports(dModule))
{
dModule->ErrorMsg = (wchar_t*)pHeapAlloc(pGetProcessHeap(), HEAP_ZERO_MEMORY, 500);
if (!dModule->ErrorMsg)
goto Cleanup;
pwsprintfW(dModule->ErrorMsg, TEXT("Failed to resolve imports: %s"), lpwName);
goto Cleanup;
}
最後就是將 DLL link 到 PEB,這也是作者文章中介紹的主要內容。
// link the module to the PEB
if (dModule->bLinkedToPeb)
{
if (!LinkModuleToPEB(dModule))
{
dModule->ErrorMsg = (wchar_t*)pHeapAlloc(pGetProcessHeap(), HEAP_ZERO_MEMORY, 500);
if (!dModule->ErrorMsg)
goto Cleanup;
pwsprintfW(dModule->ErrorMsg, TEXT("Failed to link module to PEB: %s"), lpwName);
goto Cleanup;
}
}
LinkModuleToPEB 的目的是將載入的 DLL 加入 InLoadOrderModuleList
, InMemoryOrderModuleList
, InInitializationOrderModuleList
。
首先建立一塊 LDR_DATA_TABLE_ENTRY 的結構,並且填入 DLL 相關的資訊。
BOOL LinkModuleToPEB(
PDARKMODULE pdModule
)
{
PIMAGE_NT_HEADERS pNtHeaders;
UNICODE_STRING FullDllName, BaseDllName;
PLDR_DATA_TABLE_ENTRY2 pLdrEntry = NULL;
GETPROCESSHEAP pGetProcessHeap = (GETPROCESSHEAP)GetFunctionAddress(IsModulePresent(L"Kernel32.dll"), "GetProcessHeap");
HEAPALLOC pHeapAlloc = (HEAPALLOC)GetFunctionAddress(IsModulePresent(L"Kernel32.dll"), "HeapAlloc");
RTLINITUNICODESTRING pRtlInitUnicodeString = (RTLINITUNICODESTRING)GetFunctionAddress(IsModulePresent(L"ntdll.dll"), "RtlInitUnicodeString");
NTQUERYSYSTEMTIME pNtQuerySystemTime = (NTQUERYSYSTEMTIME)GetFunctionAddress(IsModulePresent(L"ntdll.dll"), "NtQuerySystemTime");
pNtHeaders = RVA(
PIMAGE_NT_HEADERS,
pdModule->pbDllData,
((PIMAGE_DOS_HEADER)pdModule->pbDllData)->e_lfanew
);
// convert the names to unicode
pRtlInitUnicodeString(
&FullDllName,
pdModule->LocalDLLName
);
pRtlInitUnicodeString(
&BaseDllName,
pdModule->CrackedDLLName
);
// link the entry to the PEB
pLdrEntry = (PLDR_DATA_TABLE_ENTRY2)pHeapAlloc(
pGetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(LDR_DATA_TABLE_ENTRY2)
);
if (!pLdrEntry)
{
return FALSE;
}
// start setting the values in the entry
pNtQuerySystemTime(&pLdrEntry->LoadTime);
// do the obvious ones
pLdrEntry->ReferenceCount = 1;
pLdrEntry->LoadReason = LoadReasonDynamicLoad;
pLdrEntry->OriginalBase = pNtHeaders->OptionalHeader.ImageBase;
// set the hash value
pLdrEntry->BaseNameHashValue = LdrHashEntry(
BaseDllName,
FALSE
);
// correctly add the base address to the entry
AddBaseAddressEntry(
pLdrEntry,
(PVOID)pdModule->ModuleBase
);
// and the rest
pLdrEntry->ImageDll = TRUE;
pLdrEntry->LoadNotificationsSent = TRUE; // lol
pLdrEntry->EntryProcessed = TRUE;
pLdrEntry->InLegacyLists = TRUE;
pLdrEntry->InIndexes = TRUE;
pLdrEntry->ProcessAttachCalled = TRUE;
pLdrEntry->InExceptionTable = FALSE;
pLdrEntry->DllBase = (PVOID)pdModule->ModuleBase;
pLdrEntry->SizeOfImage = pNtHeaders->OptionalHeader.SizeOfImage;
pLdrEntry->TimeDateStamp = pNtHeaders->FileHeader.TimeDateStamp;
pLdrEntry->BaseDllName = BaseDllName;
pLdrEntry->FullDllName = FullDllName;
pLdrEntry->ObsoleteLoadCount = 1;
pLdrEntry->Flags = LDRP_IMAGE_DLL | LDRP_ENTRY_INSERTED | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED;
// set the correct values in the Ddag node struct
pLdrEntry->DdagNode = (PLDR_DDAG_NODE)pHeapAlloc(
pGetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(LDR_DDAG_NODE)
);
if (!pLdrEntry->DdagNode)
{
return 0;
}
pLdrEntry->NodeModuleLink.Flink = &pLdrEntry->DdagNode->Modules;
pLdrEntry->NodeModuleLink.Blink = &pLdrEntry->DdagNode->Modules;
pLdrEntry->DdagNode->Modules.Flink = &pLdrEntry->NodeModuleLink;
pLdrEntry->DdagNode->Modules.Blink = &pLdrEntry->NodeModuleLink;
pLdrEntry->DdagNode->State = LdrModulesReadyToRun;
pLdrEntry->DdagNode->LoadCount = 1;
// add the hash to the LdrpHashTable
AddHashTableEntry(
pLdrEntry
);
// set the entry point
pLdrEntry->EntryPoint = RVA(
PVOID,
pdModule->ModuleBase,
pNtHeaders->OptionalHeader.AddressOfEntryPoint
);
return TRUE;
}
其中最困難的部分是:
InLoadOrderModuleList
, InMemoryOrderModuleList
, InInitializationOrderModuleList
這個階段會做以下步驟:
// trigger tls callbacks, set permissions and call the entry point
if (!BeginExecution(dModule))
{
dModule->ErrorMsg = (wchar_t*)pHeapAlloc(pGetProcessHeap(), HEAP_ZERO_MEMORY, 500);
if (!dModule->ErrorMsg)
goto Cleanup;
pwsprintfW(dModule->ErrorMsg, TEXT("Failed to execute: %s"), lpwName);
goto Cleanup;
}
dModule->bSuccess = TRUE;
goto Cleanup;
Cleanup:
return dModule;
在 Windows 64 bits 的環境,使用 RtlAddFunctionTable 將 NT Headers→OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION] 中的 FuncEntry 加入 SEH 的 FunctionTable
DllMain
)最後,執行 DLL 的 entrypoint
測試看看 Autoruns64.dll 可不可以被載入
VOID main()
{
GETPROCESSHEAP pGetProcessHeap = (GETPROCESSHEAP)GetFunctionAddress(IsModulePresent(L"Kernel32.dll"), "GetProcessHeap");
HEAPFREE pHeapFree = (HEAPFREE)GetFunctionAddress(IsModulePresent(L"Kernel32.dll"), "HeapFree");
PDARKMODULE DarkModule = DarkLoadLibrary(
LOAD_LOCAL_FILE,
L".\\autoruns64.dll",
NULL,
0,
NULL
);
if (!DarkModule->bSuccess)
{
printf("load failed: %S\n", DarkModule->ErrorMsg);
pHeapFree(pGetProcessHeap(), 0, DarkModule->ErrorMsg);
pHeapFree(pGetProcessHeap(), 0, DarkModule);
return;
}
_ThisIsAFunction ThisIsAFunction = (_ThisIsAFunction)GetFunctionAddress(
(HMODULE)DarkModule->ModuleBase,
"AutorunScan"
);
pHeapFree(pGetProcessHeap(), 0, DarkModule);
if (!ThisIsAFunction)
{
printf("failed to find it\n");
return;
}
ThisIsAFunction(L"this is working!!!");
while (1);
return;
}
成功在 console 印出 autorun key!
雖然 Dark LoadLibrary 有不少地方還未實作,但是在分析完 Dark LoadLibrary 後,才發現自己實作完整版的 DLL Loader 還蠻困難的,要考慮非常多的細節。
下一篇開始,我會有一系列關於 Token & Object 的介紹,這些物件是 Windows 權限管理的核心,所有的權限的根源都來自於這些物件。
強欸,幾乎是把 Windows 載入 Image 所做的行為都自己實作了。
不過我可以理解 Reflective DLL Injection 可以用來繞過 LoadImageNotifyRoutine,但讓載入的 DLL 可以用 GetProcAddress 和 GetModuleHandle 的用意是什麼?